######################################## Rust 概要 ######################################## 基础语法 **************************************** Rust 是强类型语言,但是其 *let* 关键字可以充当 C++ *auto* 的角色。另外,其类型声明是后置类型声明: .. code-block:: rust let a:i32 = 10; Rust 中的变量分为三种: - 常量:使用 *const* 声明的变量,不允许更改值 - 不可变变量:使用 *let* 声明的变量,只能通过多次声明更改变量值 - 可变变量:使用 *let mut* 声明的变量,可以随意更改值 常量必须要指定类型,不可变变量在多次声明的过程中允许改变类型。子作用域中的声明不会更改父作用域变量的值 | 不允许窄化转换 | 非零值不能转换为布尔值 | 没有自增和自减运算符 数据类型 **************************************** Rust 中内置了诸如 *i32, u32* 这些宏,另外还添加了 *isize, usize* 这两个大小与 CPU 字宽度相同的类型 浮点数分为 *f32, f64* 两种,默认会使用 f64,因为两者处理速度差不多,但是 f64 精度更高 Rust 中的字符大小为 4 字节,只允许 UTF-8 编码 元组使用圆括号声明,数组使用中括号声明: .. code-block:: rust let a = (510, 1.2, 'c'); let b = [10, 20, 30]; println!("{}", a.2); println!("{}", b[0]); 注释 **************************************** 注释方式和 C++ 一样 函数 **************************************** 函数的声明方式也是后置类型声明: .. code-block:: rust fn main() { println!("{}", add(1, 2)); } fn add(a: i32, b: i32) -> i32 { return a + b; } 函数不需要前置类型声明,只要你代码中能找到这个函数就行,写前面还是写后面都行 另外,对于返回值有一种简写形式: .. code-block:: rust fn add(a: i32, b: i32) -> i32 { a + b } 需要注意的是,后面不能跟分号 条件语句 **************************************** 条件语句中的条件无需加小括号: .. code-block:: rust fn main() { let number = 3; if number < 2 { println!("number < 2"); } else if number < 5 { println!("number < 5"); } else { println!("number >= 5"); } } 另外还能实现类似 C++ 三目运算符的效果: .. code-block:: rust let number = if number > 2 { number*2} else {0}; 条件语句还有一个简写方式,通常用在枚举中: .. code-block:: rust let i = 0; if let 0 = i { println!("zero"); } 循环 **************************************** .. code-block:: rust let mut n = 1; while n != 4 { println!("{}", n); n += 1; } let a = [10, 20, 30]; for i in a { println!("{}", i); } for i in 0..5 { println!("{}", i); } let _m = loop { if n == 10 { break n; } n += 1; }; Rust 中没有 for 循环,只有 while 和 for_each 循环。另外 loop 循环相当于 while(true) 。但是不同的是可以使用 break 返回一个值 loop 在单独成块时末尾无需加分号,但是作为初始化语句的一部分时需要加分号(毕竟是语句) 这里之所以将变量命名为 `_m` 是因为我的 Rust 将 Warning 视为 Error,这里有命名 Warning 所有权 **************************************** Rust 也有作用域规则,并将表达式分为值和变量两部分,遵循以下规则: - 栈中的值默认执行拷贝语义 - 堆中的值默认执行移动语义 - 变量可以引用其它变量 例如: .. code-block:: rust #![cfg_attr( debug_assertions, allow(dead_code, unused_imports, unused_variables, unused_mut) )] fn main() { let s_a = 10; let s_b = s_a; // 执行拷贝语义 let h_a = String::from("hello"); let h_b = h_a; // 执行移动语义,此处 h_a 会失效 let mut h_c = h_b.clone(); // 执行拷贝 print_s(h_b); // 执行移动语义,此处 h_b 会失效 let r_a = &h_c; // r_a 是指向 h_c 的只读引用 let r_b = &mut h_c; // r_b 是指向 h_c 的可写引用 } fn print_s(str: String) { println!("{}", str); } 第一行代码是为了关闭 unused warning,不关掉的话代码没法通过编译 Rust 将引用的过程称为租赁 另外,Rust 没有 free 或者 delete,它会在作用域结束时自动为你添加资源清理代码 与 C++ 不同的是,引用不会影响原宿主的生命周期,当原宿主失效时,引用必须重新绑定: .. code-block:: rust #![cfg_attr( debug_assertions, allow(dead_code, unused_imports, unused_variables, unused_mut) )] fn main() { let s1 = String::from("hello"); let r = &s1; let s2 = s1; // 移交 s1 所有权, r 必须重新绑定 let r = &s2; } 悬垂引用在编译期就会被发现。 切片 **************************************** 在切片之前需要区分 str 和 String 类型。str 是 Rust 内置的基本类型,代码中使用 str 初始化的变量均为 str 的引用。String 是一种容器类型。 - 切片的结果是只读的 - 切片的类型为 &str .. code-block:: rust fn main() { let s1 = "hello"; // s1 是 &str 类型 let mut s2 = String::from("hello"); // s2 是 String 类型 let slice1 = &s2[0..3]; // slice1 是 &str 类型 let slice2 = &s2[3..]; let slice3 = &s2[..3]; println!("{}", slice1); println!("{}", slice2); println!("{}", slice3); } 结构体 **************************************** .. code-block:: rust struct Student { id: u32, name: String, } fn main() { let stu = Student { id: 1234, name: String::from("小明"), }; let stu2 = Student { id: 2345, ..stu // stu 中非 id 字段被移动 }; // println!("{}", stu.name); // 此处不允许,stu.name 已经被移动 println!("{}", stu2.name); } Rust 中的结构体无需加分号结尾,stu2 展示了另一种语法:当新的结构体 **至少** 有一个字段不同时,可以简化语法,这种语法我认为可以被成为更新 另外还有一个类元组的结构体: .. code-block:: rust struct Color(u32, u32, u32); fn main() { let black = Color(0, 0, 0); } 这种结构体的使用方式与元组相同,但是元组结构体是一个语句,末尾需要添加分号 还有两种绑定到结构体上的函数: - 如果有 &self 函数,称为方法 - 如果没有 &self 函数,可以简单地将其理解为 C++ 中的静态成员函数 .. code-block:: rust struct Int { data: u32, } impl Int { fn create(data: u32) -> Int { return Int { data }; // 这里 return 写不写都行 } fn bigger(&self, b: &Int) -> bool { return self.data > b.data; } } fn main() { let a = Int::create(10); let b = Int::create(11); println!("{}", a.bigger(&b)); } impl 可以写任意次,总的效果相当于他们的并集 还可以使用 :abbr:`特性 (trait)` 限制函数的签名,这与 C++ 中的纯虚函数类似: .. code-block:: rust struct Person { name: String, age: u8, } trait Descriptive { fn describe(&self) -> String { return String::from("[Object]"); } } impl Descriptive for Person { fn describe(&self) -> String { return format!("{} {}", self.name, self.age); } } fn output(obj: &impl Descriptive) { println!("{}", obj.describe()); } fn main() { let a = Person { name: String::from("小明"), age: 20, }; output(&a); println!("{}", a.describe()); } 特性允许有默认实现,这点和 C++ 相似。一个 impl 块只能实现一个特性 另外,接受特性的函数还有一种简便的表示方式: .. code-block:: rust fn output(obj: &T) { println!("{}", obj.describe()); } 特性可以进行组合: .. code-block:: rust fn output(obj: &T) { println!("{}", obj.describe()); } 此时类型 T 必须同时实现了 Display 和 Clone 还能使用 where 简化函数签名: .. code-block:: rust fn output(obj: &T) where T: Display + Clone, U: Clone + Debug, { println!("{}", obj); } 枚举 **************************************** 枚举的三种写法: .. code-block:: rust enum Book { Papery, Electronic, } enum Anmial { Dog(String), Cat(String), } enum Pair { Number { number: u32 }, String { str: String }, } fn main() { let book = Book::Papery; let dog = Anmial::Dog(String::from("a")); let pair = Pair::String { str: String::from("b"), }; match pair { Pair::Number { number } => { println!("Number {}", number); } Pair::String { str } => { println!("String {}", str) } } } 枚举中可以储存字段,访问这些字段只能使用 match 语法 泛型 **************************************** 简单的泛型语法: .. code-block:: rust struct Number { data: T, } impl Number { fn get_data(&self) -> &T { return &self.data; } } fn main() { let a = Number { data: 10 }; let b = Number { data: 11 }; println!("{}", a.get_data()); } 和 C++ 一样,函数也允许有泛型,这是 impl 中也需要添加相应的类型,而且泛型的具体类型可以被推断出来。推断时不会发生类型转换 错误处理 **************************************** Rust 将错误分为可恢复的错误和不可恢复的错误。 不可恢复的错误类似 C++ 中断言失败的宏: .. code-block:: rust fn main() { panic!("failed"); } 可恢复错误通过 *Result* **枚举** 表示,可能产生异常的函数的返回值都是 Result 类型的: .. code-block:: rust fn main() { let f = File::open("hello.txt"); match f{ Ok(file) =>{ println!("file opened"); }, Err(err) =>{ println!("Failed"); } } } .. hint:: 实际上报错时显示类型是 tuple 使用 if let 语法可以简化处理流程: .. code-block:: rust fn main() { let f = File::open("hello.txt"); if let Ok(file) = f { println!("File opened successfully."); } else { println!("Failed to open the file."); } } 直接对 Result 调用 unwrap/expect 会导致系统直接挂掉 因为异常只是一个类型,所以传递时直接返回就行了,异常还有一个简单语法: .. code-block:: rust fn g(i: i32) -> Result { let t = f(i)?; Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型 } 这里函数 f 的返回值是 Result,其 E 的类型必须和 g 的 E 的类型相同,? 运算符用来将异常取出,如果有异常直接返回,否则向下继续执行 生命周期参数 **************************************** 来看一个返回引用的函数的例子: .. code-block:: rust fn longer(s1: &str, s2: &str) -> &str { if s1.len() > s1.len() { return s1; } return s2; } fn main() { let s1 = "he"; let s2 = "she"; let r = longer(&s1, &s2); print!("{}", r); } 遗憾的是 Rust 不会允许编译的,根本原因在于 longer 返回的是一个引用,但是引用的值的生命周期不知道。可以通过生命周期参数解决这个问题 下面我会将术语生命周期进行加粗,而和 C++ 中生命周期相同的那个术语不会 生命周期参数的语法是一个单引号后跟一个小写字母,习惯上以 ``'a`` 表示。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。 现在使用生命周期参数来纠正这个函数: .. code-block:: rust fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s1.len() { return s1; } return s2; } 这个函数的签名分为了三个部分: - ``fn longer<'a>`` 中使用泛型的语法将生命周期参数引入函数 - ``s1: &'a str`` 表明 s1 的生命周期至少与 ``'a`` 的生命周期一样长 - ``&'a str`` 表示返回值的生命周期至少与 ``'a`` 一样长 ``'a`` 的生命周期是 s1 和 s2 中生命周期的较小值 下面无法通过编译的代码,从侧面证实了生命周期参数不会延长值的生命周期: .. code-block:: rust fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s1.len() { return s1; } return s2; } fn main() { let r; { let s1 = String::from("he"); let s2 = String::from("she"); r = longer(s1.as_str(), s2.as_str()); } print!("{}", r); } 生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str ,'static 所表示的生命周期从程序运行开始到程序运行结束,因此下面的代码可以通过编译: .. code-block:: rust let r; { let s1 = "he"; let s2 = "she"; r = longer(s1, s2); } print!("{}", r); .. note:: 生命周期参数从语法上可以简单地认为就是一个特殊的模板参数 模块、包和箱 **************************************** 简单地来讲,含有 *Cargo.toml* 文件的项目就是一个包,项目编译后生成的二进制文件就是箱 Rust 中的模块类似于 C++ 中命名空间和 Python 中模块的并集,遵循以下原则: #. 如果没有显式表明模块,则每个文件就代表一个模块(类似 Python) #. 如果显式声明模块,则遵循声明(类似命名空间) 模块允许嵌套。只有平级或者更深层次的模块才允许访问私有的函数或者结构体。如果希望外部访问,必须使用 pub 公开 模块的使用方式与命名空间类似,同样是使用 ``::`` ,但是导入运算符使用了 *use* 而不是 *using* 面向对象 **************************************** 封装可以使用模块,继承只能通过特性完成接口继承,多态也可以使用特性。但是类似 C++ 中的继承是不存在的,只能通过组合的方式 终端 IO **************************************** 例如: .. code-block:: rust use std::io::stdin; use std::io::BufRead; fn main() { let args = std::env::args(); for arg in args { println!("{}", arg); } // echo let mut buf = String::new(); stdin().read_line(&mut buf).unwrap(); println!("{}", buf); } 文件 IO **************************************** 先看一个单向读写的例子: .. code-block:: rust use std::fs; fn main() { fs::write("1.txt", "hello").unwrap(); let content = fs::read_to_string("1.txt").unwrap(); println!("{:?}", content); } 再看一个双向读写的例子: .. code-block:: rust use std::{ fs, io::{Read, Seek, Write}, }; fn main() { let mut file = std::fs::OpenOptions::new() .write(true) .read(true) .open("1.txt") .unwrap(); file.write(b"hello,world\n").unwrap(); // 刷新缓冲区 file.flush().unwrap(); file.seek(std::io::SeekFrom::Start(0)).unwrap(); // 读取数据 let mut buf = String::new(); file.read_to_string(&mut buf).unwrap(); println!("{}", buf); } 容器 **************************************** 没什么好说的,基础语法都差不多,反正有自动导入也不用担心不知道名字是什么